package net.icapsid.counter.client;

/**   
 * Based on examples from WebGL + GWT code
 * 
 * Copyright 2009-2010 Sönke Sothmann, Steffen Schäfer and others
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.util.Date;

import net.icapsid.counter.client.util.math.FloatMatrix4x4;
import net.icapsid.counter.client.util.math.MatrixUtil;
import net.icapsid.counter.client.util.math.Vector3f;
import net.icapsid.counter.client.util.math.Vectorf;
import net.icapsid.counter.client.util.mesh.Mesh;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.datepicker.client.DatePicker;
import com.googlecode.gwtgl.array.Float32Array;
import com.googlecode.gwtgl.binding.WebGLBuffer;
import com.googlecode.gwtgl.binding.WebGLCanvas;
import com.googlecode.gwtgl.binding.WebGLContextAttributes;
import com.googlecode.gwtgl.binding.WebGLProgram;
import com.googlecode.gwtgl.binding.WebGLRenderingContext;
import com.googlecode.gwtgl.binding.WebGLShader;
import com.googlecode.gwtgl.binding.WebGLUniformLocation;
import com.google.gwt.i18n.client.DateTimeFormat;

/**
 * Entry point classes define <code>onModuleLoad()</code>.
 */
public class Counter implements EntryPoint {
	
	/** The Canvas to render on. */
	protected WebGLCanvas webGLCanvas;
	
	/** The Canvas' 3D rendering context. */
	protected WebGLRenderingContext glContext;
	
	private WebGLProgram shaderProgram;
	private int vertexPositionAttribute;
	
	private WebGLBuffer vertexBuffer;
	
	private Mesh cube = new Icosahedron();

	private int angleX = 0;
	private int angleY = 0;
	private int angleZ = 0;
	private float translateZ = -2;
	private FloatMatrix4x4 perspectiveMatrix;
	private FloatMatrix4x4 translationMatrix;
	private FloatMatrix4x4 rotationMatrix;
	private FloatMatrix4x4 resultingMatrix;
	
	// Texture
	private WebGLUniformLocation projectionMatrixUniform;
	
	// Lighting
	private Vectorf lightingDirection = new Vector3f(1, 1, -3);
	
	private float ambientColor[][] = {{1.0f, 0.8f, 0.0f}, 
									  {0.37f, 0.55f, 0.82f}, 
									  {0.53f, 0.87f, 0.53f}, 
									  {1.0f, 0.16f, 0.16f}};
	
	private float directionalColorRed = 0.1f;
	private float directionalColorGreen = 0.1f;
	private float directionalColorBlue = 0.1f;
	
	// Display Elements
	private DisplayMatrix displays[] = new DisplayMatrix[9];
	short counterDigits[] = new short[displays.length];
	private FlightControl flightControl = new FlightControl(ambientColor);

	long targetDate = 0;
	
	// Store the digits
	
	int width = 0;
	int height = 0;
		
	/**
	 * Checks the WebGL Errors and throws an exception if there is an error.
	 */
	private void checkErrors() {
		
		int error = glContext.getError();
		if (error != WebGLRenderingContext.NO_ERROR) {
			String message = "WebGL Error: " + Integer.toHexString(error);
			GWT.log(message, null);
			throw new RuntimeException(message);
		}
	}

	protected void draw() {
		
//		angleX = (angleX + 1) % 360;
//		angleY = (angleY + 1) % 360;
		angleX = 30;
//		angleY = 45;
//		angleZ = 0;


		int translateX = -44;
		int translateY = 0;
		translateZ = -32;
	
//		angleZ=(angleZ+2)%360;
		
		checkErrors();

		glContext.clear(WebGLRenderingContext.COLOR_BUFFER_BIT | WebGLRenderingContext.DEPTH_BUFFER_BIT);
    	checkErrors();

		// Load the vertex data
		glContext.bindBuffer(WebGLRenderingContext.ARRAY_BUFFER, vertexBuffer);
		glContext.vertexAttribPointer(vertexPositionAttribute, 3, WebGLRenderingContext.FLOAT, false, 0, 0);

		perspectiveMatrix = MatrixUtil.createPerspectiveMatrix(45, width / (float)height, 0.1f, 100);
		translationMatrix = MatrixUtil.createTranslationMatrix(translateX, translateY, translateZ);
		rotationMatrix = MatrixUtil.createRotationMatrix(angleX, angleY, angleZ);
		resultingMatrix = perspectiveMatrix.multiply(translationMatrix).multiply(rotationMatrix);
		checkErrors();

		glContext.uniformMatrix4fv(projectionMatrixUniform, false, resultingMatrix.getColumnWiseFlatData());
		
		WebGLUniformLocation ambientColorUniform = glContext.getUniformLocation(shaderProgram, "ambientColor");
		WebGLUniformLocation lightingDirectionUniform = glContext.getUniformLocation(shaderProgram, "lightingDirection");
		WebGLUniformLocation directionalColorUniform = glContext.getUniformLocation(shaderProgram, "directionalColor");
		WebGLUniformLocation normalMatrixUniform = glContext.getUniformLocation(shaderProgram, "normalMatrix");
		
		// Lighting
		Vectorf adjustedLightDirection = lightingDirection.toUnitVector().multiply(-1);
		
		float[] flatLightDirection = adjustedLightDirection.toArray();
		glContext.uniform3f(lightingDirectionUniform, flatLightDirection[0], flatLightDirection[1], flatLightDirection[2]);
		glContext.uniform3f(directionalColorUniform, directionalColorRed, directionalColorGreen, directionalColorBlue);

		FloatMatrix4x4 normalMatrix = rotationMatrix.inverse();
		normalMatrix = normalMatrix.transpose();
		glContext.uniformMatrix4fv(normalMatrixUniform, false, normalMatrix.getColumnWiseFlatData());
		
		int points = cube.getVertexCount();
		
//		glContext.drawArrays(WebGLRenderingContext.TRIANGLES, 0, points);
		
		for(int i = 0; i < displays.length; ++i){
			
			int colorIndex = 0;
			
			if(i > 2){
				colorIndex = (i - 1) / 2;
			}
			
			glContext.uniform3f(ambientColorUniform, ambientColor[colorIndex][0], ambientColor[colorIndex][1], ambientColor[colorIndex][2]);
			displays[i].draw(glContext, points, projectionMatrixUniform, resultingMatrix, i);
		}
		
		flightControl.updatePhysics();
		flightControl.draw(glContext, points, projectionMatrixUniform, resultingMatrix, ambientColorUniform);
		
		glContext.flush();
	
		checkErrors();
	}
	
	/**
	 * Creates an Shader instance defined by the ShaderType and the source.
	 * 
	 * @param type
	 *            the type of the shader to create
	 * @param source
	 *            the source of the shader
	 * @return the created Shader instance.
	 */
	private WebGLShader getShader(int type, String source) {
		WebGLShader shader = glContext.createShader(type);
		glContext.shaderSource(shader, source);
		glContext.compileShader(shader);
		checkErrors();

		// check if the Shader is successfully compiled
		if (!glContext.getShaderParameterb(shader, WebGLRenderingContext.COMPILE_STATUS)) {
			throw new RuntimeException(glContext.getShaderInfoLog(shader));
		}

		return shader;
	}

	/**
	 * Initializes the buffers for vertex coordinates, normals and texture
	 * coordinates.
	 */
	private void initBuffers() {
		vertexBuffer = glContext.createBuffer();
		glContext.bindBuffer(WebGLRenderingContext.ARRAY_BUFFER, vertexBuffer);
		glContext.bufferData(WebGLRenderingContext.ARRAY_BUFFER,
				Float32Array.create(cube.getVertices()),
				WebGLRenderingContext.STATIC_DRAW);
	}
	
	private void initGeometry(){
		
		float deltaSmall = 9.0f;
		float deltaBig = 12.0f;
		
		float distances[] = {0f, deltaSmall, deltaSmall, deltaBig, deltaSmall, deltaBig, deltaSmall, deltaBig, deltaSmall};
		
		float deltaX = 0.0f;
		
		for(int i = 0; i < displays.length; ++i){
		
			deltaX += distances[i];
			
			displays[i] = new DisplayMatrix(flightControl);
			displays[i].setTranslation(deltaX, 0, -2);
		}
	}

	/**
	 * Initialized the params of WebGL.
	 */
	private void initParams() {
		glContext.viewport(0, 0, webGLCanvas.getOffsetWidth(), webGLCanvas.getOffsetHeight());
		
		// clear with background color
		glContext.clearColor(0.7f, 0.7f, 0.7f, 1.7f);

		// clear the whole image
		glContext.clearDepth(1.0f);

		// enable the depth test
		glContext.enable(WebGLRenderingContext.DEPTH_TEST);
		glContext.depthFunc(WebGLRenderingContext.LEQUAL);
		
		checkErrors();
	}

	/**
	 * Creates the ShaderProgram used by the example to render.
	 */
	private void initShaders() {
		// Create the Shaders
		WebGLShader fragmentShader = getShader(WebGLRenderingContext.FRAGMENT_SHADER, Resources.INSTANCE.fragmentShader().getText());
		GWT.log("Created fragment shader");
		
		WebGLShader vertexShader = getShader(WebGLRenderingContext.VERTEX_SHADER, Resources.INSTANCE.vertexShader().getText());
		GWT.log("Created vertex shader");
		if (vertexShader == null || fragmentShader == null) {
			GWT.log("Shader error");
			throw new RuntimeException("shader error");
		}

		// create the ShaderProgram and attach the Shaders
		shaderProgram = glContext.createProgram();
		if (shaderProgram == null || glContext.getError() != WebGLRenderingContext.NO_ERROR) {
			GWT.log("Program errror");
			throw new RuntimeException("program error");
		}

		GWT.log("Shader program created");
		glContext.attachShader(shaderProgram, vertexShader);
		GWT.log("vertex shader attached to shader program");
		glContext.attachShader(shaderProgram, fragmentShader);
		GWT.log("fragment shader attached to shader program");

		// Bind vertexPosition to attribute 0
		glContext.bindAttribLocation(shaderProgram, 0, "vertexPosition");

		// Link the Shader Program
		glContext.linkProgram(shaderProgram);
		if (!glContext.getProgramParameterb(shaderProgram,
				WebGLRenderingContext.LINK_STATUS)) {
			throw new RuntimeException("Could not initialise shaders: " + glContext.getProgramInfoLog (shaderProgram));
		}
		
		GWT.log("Shader program linked");
		
		// Set the ShaderProgram active
		glContext.useProgram(shaderProgram);
		
		checkErrors();
		
		vertexPositionAttribute = glContext.getAttribLocation(shaderProgram, "vertexPosition");
		glContext.enableVertexAttribArray(vertexPositionAttribute);
		
		checkErrors();
		
		// get the position of the projectionMatrix uniform.
		projectionMatrixUniform = glContext.getUniformLocation(shaderProgram,
				"projectionMatrix");
	}
	/**
	 * This is the entry point method.
	 */
	@SuppressWarnings("deprecation")
	public void onModuleLoad() {
		
		// Create the WebGL Canvas 
		WebGLContextAttributes contextAttributes = WebGLContextAttributes.create();
		contextAttributes.setAlpha(false);
		
		width = 1024; //Window.getClientWidth();
		height = 600;
		
		webGLCanvas = new WebGLCanvas(contextAttributes, Integer.toString(width) + "px",  Integer.toString(height) + "px");
		glContext = webGLCanvas.getGlContext();
		glContext.viewport(0, 0, width, height);
        RootPanel.get("gwtGL").add(webGLCanvas);

        // Create the Date picer 
        final DatePicker datePicker = new DatePicker();
        final Label text = new Label();

        // Set the default value
        datePicker.setValue(new Date(111, 4, 10), false);
        datePicker.setCurrentMonth(new Date(111, 4, 10));
        
        // Make a new list box, adding a few items to it.
        final ListBox hour = new ListBox();
        hour.setVisibleItemCount(1);

        final ListBox minute = new ListBox();
        minute.setVisibleItemCount(1);
        
        for(int i = 0; i < 24; ++i){
        	hour.addItem(Integer.toString(i));
        }
        hour.setSelectedIndex(8);
        
        for(int i = 0; i < 60; ++i){
        	minute.addItem(Integer.toString(i));
        }        
        
        FlowPanel pickerPanel = new FlowPanel();
        pickerPanel.add(datePicker);
        pickerPanel.add(hour);
        pickerPanel.add(minute);
        
        // Add the widgets to the page
        RootPanel.get("countdownTarget").add(text);
        RootPanel.get("datePicker").add(datePicker);
        RootPanel.get("timePicker").add(pickerPanel);
                
//		final Label daysLabel = new Label();
//		final Label hoursLabel = new Label();
//		final Label minutesLabel = new Label();
//		final Label secondsLabel = new Label();
//		
//		RootPanel.get("daysLabel").add(daysLabel);
//		RootPanel.get("hoursLabel").add(hoursLabel);
//		RootPanel.get("minutesLabel").add(minutesLabel);
//		RootPanel.get("secondsLabel").add(secondsLabel);
		
		final Timer countDownTimer = new Timer() {
			public void run() {

				long seconds = (targetDate - System.currentTimeMillis()) / 1000;

				if(seconds <= 0){
					this.cancel();
				}

				int days = (int) (seconds / 86400);
				seconds %= 86400;

				int hours = (int) (seconds / 3600);
				seconds %= 3600;

				int minutes = (int) (seconds / 60);
				seconds %= 60;

//				daysLabel.setText(Integer.toString(days));
//				hoursLabel.setText(Integer.toString(hours));
//				minutesLabel.setText(Integer.toString(minutes));
//				secondsLabel.setText(Long.toString(seconds));
				
				displays[0].setCurrentNumber((short) (days / 100));
				displays[1].setCurrentNumber((short) ((days % 100) / 10));
				displays[2].setCurrentNumber((short) ((days % 100) % 10));
				displays[3].setCurrentNumber((short) (hours / 10));
				displays[4].setCurrentNumber((short) (hours % 10));
				displays[5].setCurrentNumber((short) (minutes / 10));
				displays[6].setCurrentNumber((short) (minutes % 10));
				displays[7].setCurrentNumber((short) (seconds / 10));
				displays[8].setCurrentNumber((short) (seconds % 10));
				
				draw();
			}
		};
		
        
		Button resetButton = new Button("Set / Reset", new ClickHandler() {
			public void onClick(ClickEvent event) {
				String dateString = DateTimeFormat.getMediumDateFormat()
						.format(datePicker.getValue());
				text.setText(dateString);
				
				int minutes = Integer.parseInt(minute.getValue(minute.getSelectedIndex()));
				int hours = Integer.parseInt(hour.getValue(hour.getSelectedIndex()));
				
				Date val = datePicker.getValue();
				// Yes this stuff is Deprecated but it's also 4am.
				val.setHours(hours);
				val.setMinutes(minutes);
				targetDate = val.getTime();
			
				countDownTimer.scheduleRepeating(100);
			}
		});
		
        pickerPanel.add(resetButton);
		
		initGeometry();
		initParams();
		initShaders();
		initBuffers();

		draw();
	}
}
